Skip to content

runtime: fold venue adapters into the restart and poison sweeps#445

Open
mfw78 wants to merge 1 commit into
feat/m2-preset-launch-surfacefrom
feat/m2-adapters-in-sweeps
Open

runtime: fold venue adapters into the restart and poison sweeps#445
mfw78 wants to merge 1 commit into
feat/m2-preset-launch-surfacefrom
feat/m2-adapters-in-sweeps

Conversation

@mfw78

@mfw78 mfw78 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

Fold venue adapters (providers) into the supervisor's restart and poison-recovery sweeps, alongside modules. A trap in a routed adapter call marks a shared Liveness dead; the sweep restarts the provider through the module backoff policy and quarantines a crash-looper through the poison policy. A dead venue now resolves to Unavailable, distinct from UnknownVenue, and adapter_alive_count tracks live routability. Adds the flaky-venue fixture, whose submit traps on a poison chain-head sentinel and recovers once the head moves on, to drive the trap-to-recovery and poison paths end to end.

Why

Adapters currently boot once and install but sit outside the sweeps, so a trapped adapter stays dead until the process restarts, and the router can only project the trap to an internal error. Folding adapters into the sweeps recovers them automatically and lets a strategy tell an unknown venue apart from a temporarily dead one.

Closes #378

Testing

  • dead_venue_is_unavailable_not_unknown, a_dead_incumbent_is_replaced_on_reinstall in host/venue_registry.rs.
  • e2e_trapped_adapter_is_swept_and_restarts, e2e_crash_looping_adapter_is_poisoned in supervisor/tests.rs, over the new flaky-venue wasm fixture.
  • cargo fmt --all --check, cargo check --workspace --all-features, cargo clippy --workspace --all-targets --all-features -- -D warnings, cargo nextest run --workspace --all-features, cargo test --doc.

AI Assistance

Implemented with Claude Code under review.

A trap in a routed adapter call marks a shared Liveness dead; the
dispatch-time sweeps restart the provider after the module backoff
policy and quarantine a crash-looper per the poison policy. A dead
venue resolves to unavailable, distinct from unknown-venue, and
adapter_alive_count reports live routability. The flaky-venue fixture
drives the trap-to-recovery and poison paths end to end.

@lgahdl lgahdl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid end-to-end feature — verified the highest-risk correctness questions are clean: the shared Liveness handle is correctly carried through reinstall_provider's swap (no window where an unconfirmed instance gets routed to), Unavailable/UnknownVenue are both exercised by tests, poison quarantine permanently gates the restart branch, and adapter_alive_count reads liveness directly so it can't drift from the sweep's cached state. Three things worth a look:

// failure here is unreachable; skip defensively regardless.
// A dead venue fails to resolve; its watch stays for the
// cadence after the sweep restarts the adapter.
let Ok(slot) = self.resolve(&venue) else {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says a dead venue's "watch stays for the cadence after the sweep restarts" — true, the entry itself isn't evicted here. But since resolve() failing means this continues before ever reaching record_polled_status (where expires_at gets refreshed, per #438), a venue that takes multiple backoff cycles to recover never gets its watch-expiry deadline pushed out during the outage. If the outage outlasts the configured watch-expiry window, prune_expired will silently evict the pending watch for an intent whose venue just happens to still be mid-recovery — losing status tracking for that intent, not just delaying it. Worth either refreshing expires_at on a dead-venue skip too, or explicitly noting the interaction with the watch-expiry policy so it's a documented tradeoff rather than a surprise.

{
provider.alive = false;
provider.failure_count = provider.failure_count.saturating_add(1);
let backoff = crate::runtime::restart_policy::backoff_for(provider.failure_count);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Providers get byte-identical backoff/poison tuning to modules (backoff_for/poison_policy are the same engine-wide policy shared with sweep_modules) despite a strictly larger blast radius — one dead venue stalls every module routing through it, not just its own workflow. A popular shared venue (e.g. a CoW adapter serving many modules) traps under load and gets the same 1s-then-doubling curve and poison threshold as a niche single-module adapter, with no faster-detect or wider-tolerance curve reflecting how many callers are blocked on it. Worth at least a comment on why uniform tuning is acceptable at current scale, or a per-provider policy override as a follow-up.

/// liveness reports (backoff plus poison bookkeeping), then reinstall
/// dead, unpoisoned providers whose backoff has elapsed. Runs at the
/// head of every dispatch, beside the module restart sweep.
async fn sweep_providers(&mut self) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweep_providers is a hand-duplicated copy of sweep_modules's death-detection/backoff/restart control flow (not a shared generic loop over a common trait) — a future third actor kind needing sweep participation would need its own bespoke sweep_x/try_restart_x/LoadedX rather than slotting into existing infrastructure. Not blocking for this PR, but worth flagging before a third kind shows up: a Sweepable trait (is_alive/mark_checked/restart) would let sweep_modules/sweep_providers converge into one generic loop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants